﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using MailServer.POP3.Data;
using System.Threading;
using System.Xml;
using DotMail.Commons.DAO;
using DotMail.Commons.Interfaces;


namespace MailServer.POP3
{
    public class Server : PostOfficeInterface
    {
        #region Members
        private bool useSSL = false;
        private TcpListener tcpListener;
        private TcpListener sslTcpListener;
        private int portNumber = 110;
        private int sslPortNumber = 995;
        private IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
        private static string pos = "+OK ";
        private static string neg = "-ERR ";
        private static string crlf = "\r\n";
        private static int maxResponseSize = 512;
        private int maxAuthTries = 4;
        [ThreadStatic]
        private static List<ListMessage> messageList;
        #endregion

        #region Constructors
        public Server()
        {
        }

        public override void LoadConfig(XmlNode settings)   
        {
            ReadSettings(settings);
            Thread worker = new Thread(StartListening);
            worker.Start();
        }
        #endregion

        #region Methods
        private void AcceptConnection(Object obj)
        {
            if (obj.GetType() == typeof(Socket))
            {
                using (var socket = obj as Socket)
                {
                    var sessionID = GetSessionID();
                    var response = pos + "dotMail POP3 Service Version 0.1 " + sessionID + " ready";
                    var welcome = Encoding.ASCII.GetBytes(response);
                    socket.Send(welcome, welcome.Length, SocketFlags.None);
                    User user = null;
                    if (Authenticate(socket, out user))
                    {
                        bool clientQuit = TransactionState(socket, user);

                        if (clientQuit)
                        {
                            UpdateState(socket, user);
                        }
                        return;
                    }
                    SignOff(socket, sessionID);
                    return;
                }
            }
        }

        private bool Authenticate(Socket socket, out User user)
        {
            int tryCount = 0;
            bool retVal = false;
            user = null;
            do
            {
                string req = GetClientRequest(socket);
                if (req.StartsWith("USER", StringComparison.InvariantCultureIgnoreCase))
                {
                    var response = Encoding.ASCII.GetBytes(pos);
                    socket.Send(response, response.Length, SocketFlags.None);

                    var pass = GetClientRequest(socket);
                    if (pass.StartsWith("PASS", StringComparison.InvariantCultureIgnoreCase))
                    {
                        var username = req.Substring(req.IndexOf(' '));
                        var word = pass.Substring(pass.IndexOf(' '));
                        var result = Login(username, word, true);
                        if (result != null)
                        {
                            user = result;
                            retVal = true;
                            break;
                        }
                     
                    }
                    // Answer with a negative
                    tryCount++;
                    socket.Send(Encoding.ASCII.GetBytes(neg), SocketFlags.None);
                }
                else if (req.StartsWith("APOP", StringComparison.InvariantCultureIgnoreCase))
                {
                    socket.Send(Encoding.ASCII.GetBytes(neg));
                    user = null;
                }
                else if (req.StartsWith("QUIT", StringComparison.InvariantCultureIgnoreCase))
                {
                    // TODO Be polite say goodbye
                    user = null;
                    break;
                }
                else
                {
                    tryCount++;
                    socket.Send(Encoding.ASCII.GetBytes(neg), SocketFlags.None);
                }
            } while (tryCount < maxAuthTries);
            if (!retVal)
            {
                user = null;
            }
            return retVal;
        }

        private string GetClientRequest(Socket socket)
        {
            byte[] recievedBytes = new byte[maxResponseSize];
            int byteCount = socket.Receive(recievedBytes, maxResponseSize, SocketFlags.None);
            var request = new StringBuilder(Encoding.ASCII.GetString(recievedBytes));
            while (socket.Available > 0)
            {
                // Read the rest of the request
                byteCount = socket.Receive(recievedBytes, maxResponseSize, SocketFlags.None);
                request.Append(Encoding.ASCII.GetChars(recievedBytes));
            }
            return request.ToString().ToUpper();
        }

        private int GetMessageListByteCount()
        {
            int byteCount = 0;
            foreach (var message in messageList)
            {
                if (!message.Deleted)
                {
                    byteCount += message.ByteCount;
                }
            }
            return byteCount;
        }

        private string GetScanListing(int messageNumber, ListMessage message)
        {
            return string.Format("{0} {1}", messageNumber, message.ByteCount);
        }

        private string GetSessionID()
        {
            return "<" +
                Thread.CurrentThread.ManagedThreadId.ToString() + 
                DateTime.Now.Ticks.ToString() + 
                "@" + 
                Dns.GetHostName() +
                ">";
        }

        private string GetUIDLListing(int messageNumber, ListMessage message)
        {
            return string.Format("{0} {1}", messageNumber, message.UID);
        }

        private void HandleDeleRequest(Socket socket, User user, string request)
        {
            string[] commmands = request.Split(new char[] { ' ' }, 2);
            int messageNumber = 0;
            if (string.IsNullOrEmpty(commmands[1]))
            {
                SendClientResponse(string.Format("{0} no message number", neg), socket);
                return;
            }
            bool numberOk = Int32.TryParse(commmands[1], out messageNumber);
            if (!numberOk)
            {
                SendClientResponse(string.Format("{0} no message number", neg), socket);
                return;
            }

            if (messageList.Count < messageNumber || messageNumber < 0)
            {
                SendClientResponse(string.Format("{0} no such message", neg), socket);
                return;
            }

            if (messageList[messageNumber].Deleted)
            {
                SendClientResponse(string.Format("{0} message already deleted", neg), socket);
                return;
            }

            messageList[messageNumber].Deleted = true;
            if (messageList[messageNumber].Deleted)
            {
                SendClientResponse(string.Format("{0} message deleted", pos), socket);
            }
            else
            {
                SendClientResponse(string.Format("{0} message could not be deleted", neg), socket);
            }
        }

        private void HandleListRequest(Socket socket, User user, string request)
        {
            string[] reqParts = request.Split(new char[] { ' ' }, 2);
            if (string.IsNullOrEmpty(reqParts[1]))
            {
                string scanListingStart = string.Format("{0} {1} messages ({2} octets)",
                    pos, messageList.Count.ToString(), GetMessageListByteCount());
                SendClientResponse(scanListingStart, socket);

                int counter = 0;
                foreach (var message in messageList)
                {
                    if (!message.Deleted)
                    {
                        SendClientResponse(GetScanListing(counter, message), socket);
                    }
                    counter++;
                }

                SendClientResponse(crlf + ".", socket);
            }
            else
            {
                int messageNumber;
                bool parseSuccess = int.TryParse(reqParts[1], out messageNumber);
                if (parseSuccess)
                {
                    if (messageNumber >= 0 && messageNumber < messageList.Count && !messageList[messageNumber].Deleted)
                    {
                        SendClientResponse(pos + " " + GetScanListing(messageNumber, messageList[messageNumber]), socket);
                    }
                    else
                    {
                        SendClientResponse(string.Format("{0} no such message",
                            pos, messageList.Count), socket);
                    }
                }
                else
                {
                    SendClientResponse(string.Format("{0} must send a valid integer", neg), socket);
                }
            }
        }

        private void HandleNoopRequest(Socket socket)
        {
            SendClientResponse(pos, socket);
        }

        private void HandleRsetRequest(Socket socket, User user)
        {
            var messages = from message in messageList where message.Deleted select message;
            foreach (var message in messages)
            {
                message.Deleted = false;
            }

            SendClientResponse(pos, socket);
        }

        private void HandleRetrRequest(Socket socket, User user, string request)
        {
            string[] command = request.Split(new char[] { ' ' }, 2);
            int messasgeNumber = 0;
            bool gotMessageNumber = int.TryParse(command[1], out messasgeNumber);
            if (!gotMessageNumber)
            {
                SendClientResponse(string.Format("{0} no message number", neg), socket);
                return;
            }

            if (messasgeNumber >= messageList.Count || 
                messasgeNumber < 0 ||
                messageList[messasgeNumber] == null ||
                messageList[messasgeNumber].Deleted)
            {
                SendClientResponse(string.Format("{0} no such message", neg), socket);
                return;
            }

            SendClientResponse(string.Format("{0} {1} octets", pos, messageList[messasgeNumber].ByteCount), socket);
            Message message = this.GetMessage(messasgeNumber);
            SendClientResponse(message.OriginalMessage, socket);
            SendClientResponse(string.Format("\n{0}\n", pos), socket);
        }

        private void HandleStatRequest(Socket socket, User user)
        {
            int byteCount = GetMessageListByteCount();
            int messageCount = messageList.Count(m => !m.Deleted);
            string response = pos + " " + messageCount.ToString() + " " + byteCount.ToString();
            SendClientResponse(response, socket);
        }

        private void HandleTopRequest(Socket socket, User user, string request)
        {
            string[] command = request.Split(new char[] { ' ' }, 3);
            int messageNumber = 0;
            int lineCount = 0;
            bool gotMessageNumber = int.TryParse(command[1], out messageNumber);
            bool gotLineCount = int.TryParse(command[2], out lineCount);

            if (!(gotLineCount && gotMessageNumber))
            {
                SendClientResponse(string.Format("{0} invalid parameters", neg), socket);
                return;
            }

            if (lineCount < 0)
            {
                SendClientResponse(string.Format("{0} line count must be a positive number", neg), socket);
                return;
            }

            if (!VerifyMessageNumber(messageNumber))
            {
                SendClientResponse(string.Format("{0} no such message", neg), socket);
                return;
            }

            Message message = GetMessage(messageNumber);
            string[] lines = message.OriginalMessage.Split(new char[] { '\u0085', '\u2028' });

            SendClientResponse(pos, socket);

            if (lines.Length < lineCount)
            {
                SendClientResponse(message.OriginalMessage, socket);
                SendClientResponse("\n.\n", socket);
            }
            else
            {
                for (int i = 0; i < lineCount; i++)
                {
                    SendClientResponse(lines[i], socket);
                    SendClientResponse("\n.\n", socket);
                }
            }
        }

        private void HandleUidlRequest(Socket socket, User user, string request)
        {
            string[] command = request.Split(new char[] { ' ' }, 2);
            int messageNumber = 0;
            bool gotNumber = int.TryParse(command[1], out messageNumber);

            if (gotNumber)
            {
                if (messageNumber > -1 && messageNumber < messageList.Count && !messageList[messageNumber].Deleted)
                {
                    SendClientResponse(GetUIDLListing(messageNumber, messageList[messageNumber]), socket);
                }
                else
                {
                    SendClientResponse(string.Format("{0} No such message", neg), socket);
                }
            }
            else
            {
                SendClientResponse(pos, socket);

                int count = 0;
                foreach (var message in messageList)
                {
                    if (!message.Deleted)
                    {
                        SendClientResponse(GetUIDLListing(count, message), socket);
                    }
                    count++;
                }

                SendClientResponse(crlf + ".", socket);
            }
        }

        private void ReadSettings(XmlNode settings)
        {
            if (!string.IsNullOrEmpty(settings.SelectSingleNode("bindTo/port").InnerText))
            {
                portNumber = Convert.ToInt32(settings.SelectSingleNode("bindTo/port").InnerText);
            }
            if (!string.IsNullOrEmpty(settings.SelectSingleNode("useSSL").InnerText))
            {
                useSSL = Convert.ToBoolean(settings.SelectSingleNode("useSSL").InnerText);
            }
            if (useSSL)
            {
                if (!string.IsNullOrEmpty(settings.SelectSingleNode("useSSL/sslPort").InnerText))
                {
                    sslPortNumber = Convert.ToInt32(settings.SelectSingleNode("useSSL/sslPort").InnerText);
                }
            }
            if (!string.IsNullOrEmpty(settings.SelectSingleNode("ip").InnerText))
            {
                ipAddress = IPAddress.Parse(settings.SelectSingleNode("ip").InnerText);
            }

        }

        private void SendClientResponse(string response, Socket socket)
        {
            var responseBytes = Encoding.ASCII.GetBytes(response + crlf);
            socket.Send(responseBytes, responseBytes.Length, SocketFlags.None);
        }

        private void SignOff(Socket socket, string sessionID)
        {
            SendClientResponse(pos + "dotMail POP3 Service Version 0.1 " + sessionID + " signing off", socket);
            socket.Disconnect(false);
            socket.Close();
        }

        public override void StartListening()
        {
            tcpListener = new TcpListener(ipAddress, portNumber);
            if (useSSL)
            {
                sslTcpListener = new TcpListener(ipAddress, sslPortNumber);
            }
            // TODO Find out how we are going to listen to both ports at the same time, using two threads could be one way but Async binding is probably better

            if (!useSSL)
            {
                // Wire up the listening mechanism simple case
                // TODO For now we start with dreating a thread for each new connection
                tcpListener = new TcpListener(ipAddress, portNumber);
                while (true)
                {
                    var socket = tcpListener.AcceptSocket();
                    Thread comsThread = new Thread(new ParameterizedThreadStart(AcceptConnection));
                    comsThread.Start(socket);
                }
            }
        }

        private bool TransactionState(Socket socket, User user)
        {
            messageList = GetMessageList(user.ID, 1);
            try
            {
                var request = GetClientRequest(socket);
                if (request.Equals("QUIT"))
                {
                    return true;
                }
                do
                {
                    switch (request.Substring(request.IndexOf(' ')))
                    {
                        case "STAT":
                            HandleStatRequest(socket, user);
                            break;
                        case "LIST":
                            HandleListRequest(socket, user, request);
                            break;
                        case "RETR":
                            HandleRetrRequest(socket, user, request);
                            break;
                        case "DELE":
                            HandleDeleRequest(socket, user, request);
                            break;
                        case "NOOP":
                            HandleNoopRequest(socket);
                            break;
                        case "RSET":
                            HandleRsetRequest(socket, user);
                            break;
                        case "TOP":
                            HandleTopRequest(socket, user, request);
                            break;
                        case "UIDL":
                            HandleUidlRequest(socket, user, request);
                            break;
                        default:
                            SendClientResponse(neg, socket);
                            break;
                    }
                    request = GetClientRequest(socket);
                } while (!request.Equals("QUIT"));

                return true;
            }
            catch (SocketException ex)
            {
                // TODO Add code to log error
                return false;
            }
            catch(ObjectDisposedException ex)
            {
                // TODO Add code to log error
                return false;
            }
        }

        private void UpdateState(Socket socket, User user)
        {
            throw new NotImplementedException();
        }

        private bool VerifyMessageNumber(int messageNumber)
        {
            if (messageNumber < 0)
                return false;
            if (messageNumber >= messageList.Count)
                return false;
            if (messageList[messageNumber].Deleted)
                return false;

            return true;
        }

        #endregion

        #region PostOfficeInterface Events

        public override event getMessageList GetMessageList;

        public override event getFolderList GetFolderList;

        public override event getMessage GetMessage;

        public override event login Login;

        public override event logout LogOut;

        public override event changeMessageAttributes ChangeMessageAttributes;

        public override event copyMessage MoveMessage;

        public override event deleteMessage DeleteMessage;

        public override event createFolder CreateFolder;

        public override event getMessageAttributes GetMessageAttributes;

        public override event changeFolder ChangeFolder;

        #endregion
    }
}
